Skip to content

fix: Resolve Web Crypto AES-GCM Map conversion bug during encryption#27

Open
Shubhashish-Chakraborty wants to merge 3 commits intoalphaonelabs:mainfrom
Shubhashish-Chakraborty:fix/web-crypto-aes-object-conversion
Open

fix: Resolve Web Crypto AES-GCM Map conversion bug during encryption#27
Shubhashish-Chakraborty wants to merge 3 commits intoalphaonelabs:mainfrom
Shubhashish-Chakraborty:fix/web-crypto-aes-object-conversion

Conversation

@Shubhashish-Chakraborty
Copy link
Copy Markdown
Contributor

@Shubhashish-Chakraborty Shubhashish-Chakraborty commented Mar 27, 2026

This PR resolves registration failures (NotSupportedError: Unrecognized key import algorithm "undefined" requested.), occurred while registering a new user.

By Explicitly converting Python dictionaries to standard JavaScript Objects using dict_converter=js.Object.fromEntries when passing cryptographic algorithm parameters to js.crypto.subtle, preventing Pyodide from mistakenly converting them into Map objects.

image

Summary

This PR fixes a Pyodide ↔ Web Crypto AES-GCM interop bug that caused registration failures with NotSupportedError: Unrecognized key import algorithm "undefined". Pyodide was marshaling Python dicts as JS Maps, which resulted in an undefined algorithm being passed to the Web Crypto API. The change ensures algorithm parameters are converted to plain JS Objects so AES-256-GCM key import/encryption works correctly.

Key changes (src/worker.py)

  • _import_aes_key

    • Build the algorithm parameter as a plain JS Object using dict_converter=js.Object.fromEntries when calling to_js, preventing Map conversion.
    • Remove reliance on previously used create_pyproxies=False for algorithm marshaling.
  • encrypt_aes

    • Allocate IV as a native JS Uint8Array(12) and fill it with js.crypto.getRandomValues.
    • Convert the IV back to Python bytes for storage.
    • Marshal the algorithm with dict_converter=js.Object.fromEntries (e.g., {"name":"AES-GCM","iv": iv_array}).
    • Simplify plaintext/ciphertext marshaling by using to_js without the older create_pyproxies=False flags.
  • decrypt_aes

    • Convert IV and algorithm via to_js with dict_converter=js.Object.fromEntries.
    • Convert ciphertext via to_js (simplified marshaling).
    • Derive/import key inside the decryption flow (removing precomputed key artifacts) and preserve existing error handling (returns "[decryption error]" on failure).

Impact

  • Ensures the Web Crypto API receives plain JS Objects (not Maps), eliminating the undefined algorithm error.
  • Restores AES-256-GCM key import/encryption and fixes registration failures caused by the bug.
  • No public API changes; backward compatibility maintained for existing encrypted data and error behaviors.

Lines changed: +19 / -16

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Walkthrough

Reworked Python↔JavaScript Web Crypto AES‑GCM interop: algorithm objects are built via js.Object.fromEntries, data marshaling no longer uses create_pyproxies=False, IV is generated as a JS Uint8Array filled with crypto.getRandomValues then converted to Python bytes, and decrypt derives key/algo/data inside the try block.

Changes

Cohort / File(s) Summary
AES‑GCM marshaling & IV handling
src/worker.py
Reworked _import_aes_key, encrypt_aes, and decrypt_aes: algorithm dicts are converted with dict_converter=js.Object.fromEntries; removed create_pyproxies=False from marshaling calls; IV is allocated as js.Uint8Array(12) and filled via js.crypto.getRandomValues then converted to Python bytes; decrypt_aes now derives key/algo/data inside the try block and preserves previous decryption-error return value.

Sequence Diagram(s)

sequenceDiagram
    participant Py as Python (src/worker.py)
    participant Bridge as Pyodide JS bridge
    participant WC as WebCrypto (js.crypto)

    Py->>Bridge: build algo via dict_converter=js.Object.fromEntries
    Py->>Bridge: to_js(plaintext/ciphertext) (no create_pyproxies flag)
    Py->>Bridge: allocate iv = new Uint8Array(12)
    Bridge->>WC: crypto.getRandomValues(iv)
    Bridge->>Py: iv -> bytes (store)
    Py->>Bridge: importKey / encrypt / decrypt with algo + iv
    Bridge->>WC: perform encrypt/decrypt
    WC-->>Bridge: result (ciphertext/plaintext)
    Bridge-->>Py: return bytes or raise error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing a Web Crypto AES-GCM Map conversion bug that was causing encryption failures during user registration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/worker.py`:
- Around line 166-172: The IV conversion in the decrypt path should be made
symmetrical to encrypt: instead of wrapping iv inline with to_js in the algo
creation, convert it once (e.g., create iv_array = to_js(iv)) and then pass
iv_array into algo creation so that the decrypt logic mirrors encrypt's use of a
JS Uint8Array; update the decrypt block that calls _derive_aes_key_bytes,
_import_aes_key and js.crypto.subtle.decrypt to use this iv_array when building
algo (referencing the existing names iv, to_js, algo, and
js.crypto.subtle.decrypt).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0ef6e43-ef37-4d4b-9003-786cd9580fca

📥 Commits

Reviewing files that changed from the base of the PR and between d6e1a9c and 21fc4f2.

📒 Files selected for processing (1)
  • src/worker.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/worker.py (1)

120-148: ⚠️ Potential issue | 🟡 Minor

Test coverage for the encryption functions would strengthen confidence in security and interoperability.

The encrypt_aes and decrypt_aes functions are critical to this application—they're used throughout the worker to protect sensitive user data (usernames, emails, roles, descriptions, locations). Currently, no tests exist in the repository. Since this is a Cloudflare Workers application running on Pyodide, consider adding integration tests that exercise these functions with various inputs (empty strings, Unicode, large payloads) to catch regressions in the Pyodide↔Web Crypto interop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/worker.py` around lines 120 - 148, Add pytest integration tests that
exercise encrypt_aes and decrypt_aes end-to-end: write tests that import
encrypt_aes and decrypt_aes and verify round-trip correctness for empty string,
multi-byte Unicode, large payloads, and various secret values; include a test
that asserts a RuntimeError is raised and capture_exception is called when the
Web Crypto interop fails (simulate by mocking _import_aes_key or
js.crypto.subtle.encrypt to raise). Use mocks for Pyodide/js interop (patch js,
pyodide.ffi.to_js, _derive_aes_key_bytes, and _import_aes_key) to run in CI
without a browser, and add one integration test that runs against a real
Pyodide/Web Crypto environment if available; ensure tests cover base64 output
format ("v1:" prefix) and verify IV+ciphertext decoding during decryption.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/worker.py`:
- Around line 120-148: Add pytest integration tests that exercise encrypt_aes
and decrypt_aes end-to-end: write tests that import encrypt_aes and decrypt_aes
and verify round-trip correctness for empty string, multi-byte Unicode, large
payloads, and various secret values; include a test that asserts a RuntimeError
is raised and capture_exception is called when the Web Crypto interop fails
(simulate by mocking _import_aes_key or js.crypto.subtle.encrypt to raise). Use
mocks for Pyodide/js interop (patch js, pyodide.ffi.to_js,
_derive_aes_key_bytes, and _import_aes_key) to run in CI without a browser, and
add one integration test that runs against a real Pyodide/Web Crypto environment
if available; ensure tests cover base64 output format ("v1:" prefix) and verify
IV+ciphertext decoding during decryption.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: efa163d4-596a-422a-a422-7ee93f960f0e

📥 Commits

Reviewing files that changed from the base of the PR and between 21fc4f2 and da6f163.

📒 Files selected for processing (1)
  • src/worker.py

@AndyVale AndyVale mentioned this pull request Mar 27, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/worker.py (1)

120-173: ⚠️ Potential issue | 🟠 Major

Add a regression test for the Web Crypto interop bridge.

The repository currently has no test coverage for encrypt_aes and decrypt_aes. Given that these functions power critical authentication endpoints (api_register, api_login, and others) and depend on Pyodide's dict_converter=js.Object.fromEntries to properly handle the Web Crypto API, a regression test is essential to prevent future breakage.

A round-trip test exercising encrypt_aes/decrypt_aes or an api_register flow test would provide the safest coverage for this environment-specific interop fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/worker.py` around lines 120 - 173, Add a regression test that performs a
full round-trip using encrypt_aes and decrypt_aes (and optionally an
api_register/api_login flow) to exercise the Pyodide Web Crypto interop path:
call encrypt_aes(plaintext, secret) with a non-empty plaintext and secret,
verify the result starts with "v1:", then pass the ciphertext into
decrypt_aes(ciphertext, secret) and assert the original plaintext is returned;
include tests for empty-string short-circuiting and for invalid/malformed
ciphertext to assert "[decryption error]" is returned; ensure the test
environment uses the same dict_converter=js.Object.fromEntries behavior (so the
Web Crypto path that constructs algo via to_js({"name":"AES-GCM","iv":
iv_array}, dict_converter=js.Object.fromEntries) is exercised) and run under the
same Pyodide test harness used by other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/worker.py`:
- Around line 120-173: Add a regression test that performs a full round-trip
using encrypt_aes and decrypt_aes (and optionally an api_register/api_login
flow) to exercise the Pyodide Web Crypto interop path: call
encrypt_aes(plaintext, secret) with a non-empty plaintext and secret, verify the
result starts with "v1:", then pass the ciphertext into decrypt_aes(ciphertext,
secret) and assert the original plaintext is returned; include tests for
empty-string short-circuiting and for invalid/malformed ciphertext to assert
"[decryption error]" is returned; ensure the test environment uses the same
dict_converter=js.Object.fromEntries behavior (so the Web Crypto path that
constructs algo via to_js({"name":"AES-GCM","iv": iv_array},
dict_converter=js.Object.fromEntries) is exercised) and run under the same
Pyodide test harness used by other tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 964d622b-b734-4fcd-a8a7-b6cdcbdb9b94

📥 Commits

Reviewing files that changed from the base of the PR and between da6f163 and 2973098.

📒 Files selected for processing (1)
  • src/worker.py

Copy link
Copy Markdown
Contributor

@ayesha1145 ayesha1145 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! The dict_converter=js.Object.fromEntries fix is exactly right — Pyodide marshals Python dicts as JS Maps by default, which breaks the Web Crypto API since it expects plain JS Objects for algorithm parameters. The IV handling improvement is also cleaner. LGTM ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants